/* Sentinel - IRC Statistical and Operator Services
** users.c - Functions to track users state
**
** Copyright W. Campbell and others.  See README for more details
** Some code Copyright: Jonathan George, Kai Seidler, ircd-hybrid Team,
**                      IRCnet IRCD developers.
**
** $Id: users.c,v 1.168 2004/02/16 02:31:10 wcampbel Exp $
*/

#include <assert.h>
#include "stats.h"
#include "struct.h"
#include "proto.h"
#include "match.h"
#include "slog.h"
#include "mem.h"

static struct User *new_user(char *);

dlink_list userhash[U_TABLE_SIZE];

static struct User *new_user(char *nick)
{
  struct User *u;
  dlink_node *dl = dlink_create();

  u = smalloc(sizeof(struct User));
  if (!nick)
    nick = "";
  strlcpy(u->nick, nick, MAXNICK + 1);
  u->hash = HASH((unsigned char *) nick, U_TABLE_SIZE);
  dlink_add(u, dl, &userhash[u->hash]);

  return u;
}

struct User *AddUser(char *nick, char *user, char *host, struct Server *server,
                           char *gecos)
{
  struct User *u;

  debug("AddUser(): %s (%s@%s) -> %s", nick, user, host,
        server->name);

  u = finduser(nick);
  if (u)
  {
    slog(DEFAULT_LOG, L_WARNING, "trying to add a user that already exists? (%s)",
         nick);
    return u;
  }

  u = new_user(nick);
  strlcpy(u->hostname, host, MAXHOST);
  strlcpy(u->username, user, MAXUSER + 1);
  u->server = server;
  u->t_flood = CurrentTime;
  u->tld = AddTLD(u);
#ifdef TRACK_GECOS

  if (gecos != NULL)
  {
    u->gecos = sstrdup(gecos);
    memcounts.gecos++;
    memcounts.gecosmem += strlen(gecos) + 1;
  }
#endif

  if (!u->server)
    slog(DEFAULT_LOG, L_WARNING, "Unable to find server for user %s (%s?)", nick,
         server);
  else
    IncreaseUsers(u->server);

  netinfo.connections++;
  daily.connections++;

#ifdef MONSERV

  AddUserToHashes(u);
#ifdef MON_DRONES

  CheckDrones(u);
  if (gecos != NULL)
    CheckGecosDrones(u, gecos);
#endif
#ifdef TRACK_CTCPVERSION

  if (u->server == me.s)
  {
    char tmpver[BUFSIZE];

    snprintf(tmpver, BUFSIZE, "%s %s -> %s", stats_version, VersionFlags(),
             stats_date);

    u->version = sstrdup(tmpver);
    memcounts.cversion++;
    memcounts.cversionmem += strlen(u->version) + 1;
  }
  else
  {
    if (mn_delayed)
    {
      dlink_node *dl;

      dl = dlink_create();
      dlink_add(u, dl, &delayed_users);
    }
    else
    {
      sts(":%s PRIVMSG %s :\001VERSION\001", MonServ.nick, u->nick);
    }
  }
#endif
#endif

  return u;
}

void DelUser(char *nick)
{
  struct User *u = finduser(nick);

  if (!u)
  {
    slog(DEFAULT_LOG, L_WARNING, "DelUser(%s) failed!", nick);
    return ;
  }
  DelUserU(u);
}

void DelUserU(struct User *u)
{
  dlink_node *dl;
  dlink_node *tdl;
  struct ChanUser *cu;

  debug("DelUserU(%s)", u->nick);

  if (u == NULL)
  {
    /* assert? */
    return ;
  }

  dl = dlink_find_delete(u, &userhash[u->hash]);
  dlink_free(dl);

  DecreaseUsers(u->server);

  if (u->tld)
    u->tld->users--;
  if (IsAway(u))
  {
    netinfo.away--;
    u->server->away--;
  }
  if (IsOper(u))
  {
    netinfo.opers--;
    u->server->opers--;
    DelOperFromList(&global_oper_list, u);
  }
  if (IsInvis(u))
  {
    if (netinfo.invis)
      netinfo.invis--;
    if (u->server->invis)
      u->server->invis--;
  }
  if (u->myuser)
  {
    u->myuser->flags &= ~MFLAG_ISON;
    /* If the myuser struct is marked as a dying
    ** one, and they aren't logged in via DCC,
    ** remove it.
    */
    if (IsOperDying(u->myuser) && !IsOperDCC(u->myuser))
    {
      tdl = dlink_find_delete(u->myuser, &myudying);
      dlink_free(tdl);
#ifdef HAVE_LIBCRYPTO

      if (u->myuser->response != NULL)
        free(u->myuser->response);
      if (u->myuser->keyfile != NULL)
        RSA_free(u->myuser->keyfile);
#endif

      free(u->myuser);
      memcounts.myuser--;
    }
  }

  /* Remove the user from each channel */
  DLINK_FOREACH_SAFE(dl, tdl, u->channels.head)
  {
    cu = (struct ChanUser *) dl->data;
    DelUserFromChannel(cu->chan, u);
    /* The ChanUser is free'd as a side effect of
    ** DelUserFromChannel()
    */
    dlink_delete(dl, &u->channels);
    dlink_free(dl);
  }

  /* Debug asserts */
  assert(u->tld->users >= 0);
  assert(u->server->opers >= 0);
  assert(netinfo.opers >= 0);
  assert(netinfo.users >= 0);

#ifdef TRACK_GECOS

  if (u->gecos != NULL)
  {
    memcounts.gecos--;
    memcounts.gecosmem -= strlen(u->gecos) + 1;
    free(u->gecos);
  }
#endif

#ifdef MONSERV
  DelUserFromHashes(u);
#ifdef MON_DRONES

  RemoveDrone(u);
#endif
#endif

#ifdef TRACK_CTCPVERSION

  if (u->version != NULL)
  {
    memcounts.cversion--;
    memcounts.cversionmem -= strlen(u->version) + 1;
    free(u->version);
  }

  /* _SAFE really isn't necessary */
  DLINK_FOREACH_SAFE(dl, tdl, delayed_users.head)
  {
    if (u == dl->data)
    {
      dlink_delete(dl, &delayed_users);
      dlink_free(dl);
      break;
    }
  }
#endif

  free(u);
}

#if defined(IRCD_BAHAMUT) || defined(IRCD_HYBSVS)
void Change_User(struct User *u, char *newnick, int issvs)
#else
void Change_User(struct User *u, char *newnick)
#endif
{
  dlink_node *dl;

  debug("Change_User(%s, %s)", u->nick, newnick);

#if defined(IRCD_BAHAMUT) || defined(IRCD_HYBSVS)

  spy("%s Change by %s to %s", issvs ? "SVSNICK" : "NICK", u->nick,
      newnick);
  if (issvs)
    sendto_dcc_flag(CF_SERVICES, "SVSNICK Change by %s to %s",
                    u->nick, newnick);
  else
    sendto_dcc_flag(CF_USER, "NICK Change by %s to %s",
                    u->nick, newnick);
#else

  spy("NICK Change by %s to %s", u->nick, newnick);
  sendto_dcc_flag(CF_USER, "NICK Change by %s to %s", u->nick, newnick);
#endif

  dl = dlink_find_delete(u, &userhash[u->hash]);
  /* assert rather than free, we will reuse the dlink_node */
  assert(dl != NULL);

  strlcpy(u->nick, newnick, MAXNICK + 1);

  u->hash = HASH((unsigned char *) u->nick, U_TABLE_SIZE);
  dlink_add(u, dl, &userhash[u->hash]);
}

#ifdef SUPPORT_HOSTSERV
void Change_Host(struct User *u, char *newhost)
{
  debug("Change_Host(%s, %s)", u->hostname, newhost);

  spy("SVSHOST Change by %s to %s", u->hostname, newhost);
  sendto_dcc_flag(CF_SERVICES, "SVSHOST Change By %s to %s",
                  u->hostname, newhost);

  strlcpy(u->hostname, newhost, MAXHOST);
  u->tld = AddTLD(u);
}

void Change_Ident(struct User *u, char *newident)
{
  debug("Change_Host(%s, %s)", u->hostname, newident);

  spy("SVSIDENt Change by %s to %s", u->username, newident);
  sendto_dcc_flag(CF_SERVICES, "SVSIDENT Change By %s to %s",
                  u->username, newident);

  strlcpy(u->username, newident, MAXUSER + 1);
}
#endif

struct User *finduser(char *nick)
{
  struct User *u;
  dlink_node *dl;
  unsigned long hv;

  hv = HASH((unsigned char *) nick, U_TABLE_SIZE);

  debug("finduser(%s)", nick); /* Result useful? */
  DLINK_FOREACH(dl, userhash[hv].head)
  {
    u = dl->data;

    if (irccasecmp(u->nick, nick) == 0)
      return u;
  }

  return NULL;
}

void init_user_hash()
{
  dlink_node *dl, *tdl;
  int i;
  struct User *u;

  for (i = 0; i < U_TABLE_SIZE; i++)
  {
    DLINK_FOREACH_SAFE(dl, tdl, userhash[i].head)
    {
      u = dl->data;

      if (u != NULL)
      {
        debug("init_user_hash() deleting %s", u->nick);
        DelUserU(u);
      }
      dlink_free(dl);
    }
    memset(&userhash[i], 0, sizeof(dlink_list));
  }
}

void UserMode(struct User *u, char *modes)
{
  int toadd = 0;

  if (!u)
  {
    slog(DEFAULT_LOG, L_WARNING, "Changing modes for unknown user");
    return ;
  }

  while (*modes != '\0')
  {
    switch (*modes)
    {
      case '+':
        toadd = 1;
        break;
      case '-':
        toadd = 0;
        break;
      case 'o':
        if (toadd)
        {
          if (!IsOper(u))
          {
            set_oper(u, UFLAG_ISOPER, OPER_GLOBAL);

            alert("New Oper %s on %s", u->nick,
                  u->server->name);
            sendto_dcc_flag(CF_OPER,
                            "New Oper %s on %s", u->nick,
                            u->server->name);
            debug("%s is now an IRCop.", u->nick);
          }
        }
        else
        {
          if (IsOper(u))
          {
            u->flags &= ~UFLAG_ISOPER;
            DelOperFromList(&global_oper_list, u);
            netinfo.opers--;
            u->server->opers--;
#ifdef OPER_MODES

            u->operflags = 0;
#endif

            alert("Deoper by %s on %s", u->nick,
                  u->server->name);
            sendto_dcc_flag(CF_OPER,
                            "Deoper by %s on %s", u->nick,
                            u->server->name);
            debug("%s is no longer an IRCop.",
                  u->nick);
            assert(netinfo.opers >= 0);
            assert(u->server->opers >= 0);
          }
        }
        break;
#ifdef OPER_MODES
#ifdef IRCD_BAHAMUT

      case 'A':
#else

      case 'a':
#endif

        if (toadd)
          u->operflags |= OPER_ADMIN;
        else
          u->operflags &= ~OPER_ADMIN;
        if (toadd)
        {
          alert("New Server Admin %s on %s",
                u->nick, u->server->name);
          sendto_dcc_flag(CF_OPER,
                          "New Server Admin %s on %s",
                          u->nick, u->server->name);
        }
        else
        {
          alert("Server Admin Status Removal %s "
                "on %s", u->nick,
                u->server->name);
          sendto_dcc_flag(CF_OPER,
                          "New Server Admin %s on %s",
                          u->nick, u->server->name);
        }
        break;
#ifdef IRCD_BAHAMUT

      case 'a':
        if (toadd)
          u->operflags |= OPER_SVSADMIN;
        else
          u->operflags &= ~OPER_SVSADMIN;
        if (toadd)
        {
          alert("New Services Admin %s on %s",
                u->nick, u->server->name);
          sendto_dcc_flag(CF_OPER,
                          "New Services Admin %s on %s",
                          u->nick, u->server->name);
        }
        else
        {
          alert("Services Admin Status Removal "
                "%s on %s", u->nick,
                u->server->name);
          sendto_dcc_flag(CF_OPER,
                          "Services Admin Status Removal "
                          "%s on %s", u->nick,
                          u->server->name);
        }
        break;
#endif
#endif

      case 'O':
        /* Ignore locals */
        break;

      case 'i':
        if (toadd)
        {
          u->flags |= UFLAG_INVIS;
          netinfo.invis++;
          u->server->invis++;
        }
        else
        {
          u->flags &= ~UFLAG_INVIS;
          if (netinfo.invis)
            netinfo.invis--;
          if (u->server->invis)
            u->server->invis--;
        }
        break;

      default:
        break;
    }
    modes++;
  }
}

void IncreaseUsers(struct Server *s)
{
  debug("Increasing UserCount for %s", s->name);
  s->users++;
  netinfo.users++;
  if (s->users > s->stats->maxusers)
  {
    s->stats->maxusers = s->users;
    s->stats->t_maxusers = CurrentTime;
  }
  if (netinfo.users > netinfo.maxusers)
  {
    netinfo.maxusers = netinfo.users;
    netinfo.t_maxusers = CurrentTime;
  }
  if (netinfo.users > daily.users)
  {
    daily.users = netinfo.users;
    daily.t_users = CurrentTime;
  }
}

